Repository navigation
fix: export visible replay completions - #869
Open
zhongkechen wants to merge 5 commits into
Open
zhongkechen wants to merge 5 commits into
zhongkechen wants to merge 5 commits into
Conversation
3 tasks done
Retain terminal input and checkpoint updates until their parent registers or invocation finalization exports them. Preserve actual replay parent contexts and close each reopened invocation segment independently. Share completion claims, pending state, and successful-delivery tracking between both OTel views. Store payloads once, index children by ID, and retain completed input parents as an ID set. Validation: 901 OTel tests, type checking, formatting, and installed-wheel OTel/lifecycle validation. Twelve public-handler regressions fail against the base implementation because the callback completion is missing. Fixes #854
zhongkechen
force-pushed
the
codex/fix-854-otel-replay-completions
branch
from
October 28, 2026 18:54
01e7d73 to
626c175
Compare
This was referenced Oct 28, 2026
This was referenced Nov 2, 2026
Merge main while retaining completion delivery bookkeeping and completed-parent contexts in both OpenTelemetry views. Use the normalized terminal timestamp for exception events and span endings.
This comment has been minimized.
This comment has been minimized.
zhongkechen
commented
Nov 2, 2026
zhongkechen
left a comment
Contributor
Author
There was a problem hiding this comment.
Static review of the completion-snapshot delivery paths, against head
2b18fa1d. Three comments, all in the new drain code; the first two are the
same missing try/except and are the ones I would block on. The third is a
behaviour-change note rather than a defect.
Checked and cleared, for the record:
_completion_parent_span's(A or B) if c else Dparses as intended in both
plugins (the conditional binds looser thanor).CompletionState'spending/children_by_parentstay consistent --
take_childrencannotKeyError, sincecomplete()andclear()are the
only removals frompendingand both clean the index under the lock.- The
repeat=ends_active_contextgate is correct: ReplayChildren and virtual
child contexts both end viaon_child_context_end, which always emits
start_time=None, soends_active_contextis satisfiable and the re-entered
segment does get its durable status. parent_firstonly yields keys present in the map, so
operations[operation_id]is safe._TERMINAL_STATUSESmatches the SDK's_is_terminal_statusexactly.- Dropping
_operation_spans_lockfrom_start_spanis not a span-ID race:
DeterministicIdGenerator._id_overrideis aContextVar, and
_next_ordered_timestamptakes the lock itself. - Leaving the placeholder registered before
_start_spanis harmless --
DurableParentSpan.is_recording()isFalse, so the re-entry guard does not
end it. - Re-export of already-completed operations on a retried invocation is
pre-existing (the plugin docs note a retry sees the same updated operations),
as is the end hook firing for a terminalEXECUTIONoperation. Neither is
introduced here.
Tests were not run.
Both views drain pending completions as the first step of invocation End. Neither contains a per-completion failure, so one unexportable completion takes the Invocation span, the spans a suspended operation left open and the Workflow span with it. Two tests, failing on the current code in both views: - test_failing_processor_at_end_still_closes_the_invocation: a span processor that rejects the completion's span and stays failing through invocation End, so no retry can succeed. Asserts the completion is lost but Invocation and Workflow still close. - test_partial_start_releases_pending_metadata_at_end already asserted the defect as intended behaviour: a start that never resolved an execution trace context leaves _fallback_parent with nothing to anchor to, and the test expected that ValueError to escape on_invocation_end. It now expects the error to be reported and the invocation to finish releasing its scope.
Root cause: _drain_pending_completions runs as the first statement of _end_invocation_spans in both views and renders every pending completion uncontained. A raising span processor, or a _fallback_parent with no execution trace context to anchor to, propagates out of _end_invocation_spans. on_invocation_end's try/finally still releases the scope and flushes, but the Invocation span, the spans a suspended operation left open and the Workflow span are never ended, so the SDK logs and swallows the hook error and the whole invocation's telemetry is lost -- including the completions the drain had already rendered. Contain each completion the way a parent registration drain already does. A failed render leaves the completion pending and its claim released, so finalization continues with the next one. Simpler options considered, per CONTRIBUTING rule 2: - Move the containment into _render_operation_end so both drains share one error boundary and _drain_registered_children loses its try/except. Rejected: it would not cover _fallback_parent, which is called by the finalization drain before _render_operation_end and is one of the two ways this aborts; and it would change the natural on_operation_end path, where the SDK is already the error boundary. - Merge the two drains into one method that always resolves a fallback parent. Rejected: on the registration path a parent that is momentarily absent is currently deferred and retried, and always resolving a fallback would export it under a synthetic parent instead. parent_first can raise CycleError, which is left uncontained: a durable operation tree has no parent cycle, so per CONTRIBUTING rule 4 that guard would have no test that fails without it.
This comment has been minimized.
This comment has been minimized.
Assert sibling retention and empty-parent pruning before invocation finalization can mask either broken transition. Verify child spans before cleanup and preserve the final no-duplicate assertions.
Contributor
Codex AI reviewNo actionable findings after reviewing the full diff against CONTRIBUTING rules 1–9. Both prior findings are addressed. Tests were inspected but not executed; deployed-service behavior remains unverified. Reviewed commit |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
Fixes #854.
Description of changes
A callback can complete while an execution is suspended, then be skipped when replay returns its parent's cached result. Both OTel views now retain visible terminal updates from invocation input and checkpoint changes, deliver them through the existing terminal span path, and export unvisited completions before Invocation closes.
Parent registration drains deferred children under the actual replay segment. Completed parent contexts remain available for later updates, and finalization materializes pending ancestors before descendants. Successful observations are deduplicated within an invocation; each newly opened Invocation-view ReplayChildren or virtual-context segment still receives its own natural End.
Design and simplification
CompletionStateowns pending updates, the parent index, in-flight claims, successful-delivery bookkeeping, and cleanup for both views. Completion payloads have one authoritative dictionary; the parent index stores only IDs and preserves observation order. Completed input parents are retained as an ID set.The parent index tracks only children awaiting delivery. Completing one child removes only its ID; completing the last indexed child removes the parent bucket. Keeping empty buckets until invocation teardown would avoid the pruning branch but retain parent-index entries with no pending child. Pruning preserves this state invariant during the invocation; it does not itself change span delivery. The two-view sibling regression asserts both transitions before finalization can recover a sibling from
pendingor clear the index.The root cause is that natural End hooks cannot observe children skipped by replay, active-span registries discard completed parent identities, and operation-level deduplication alone cannot distinguish reopened context segments. This is an internal bug fix using existing snapshots and hooks; it adds no public API or configuration. Core scheduling, status mapping, timestamps, provider flushing, testing-library behavior, and conformance handlers are unchanged. Export recovery and broader finalization failure isolation remain #855.
Validation
Implementation validation (Python 3.13.17, Hatch 1.16.5):
e42947e8: all 12 cases fail because the callback span is absent.hatch run dev-otel:test -q --tb=short: 901 passed.hatch run dev-otel:typecheck: 52 source files clean.hatch fmt --checkin the OTel package andgit diff --check: passed.hatch build.hatch run test-wheel-otel:test -q --tb=short: 901 OTel tests and 12 lifecycle cases passed, with installed wheel hashes and source bytes verified.Coverage includes input/checkpoint/natural deduplication, terminal statuses, concurrent End delivery, unavailable ancestors, actual replay-parent preference, late updates, context-segment re-entry, cached-parent body skipping, unknown updated IDs, warm reuse, overlapping invocations, and child-processor isolation. Added a two-view regression that completing one child preserves its pending sibling, and strengthened the processor-failure test to assert eventual delivery exactly once within the tested invocation.
Existing behavioral expectations are preserved. Internal-state assertions in
test_completion_snapshots.py,test_context_segment_reentry.py,test_terminal_input_parent_refs.py, ande2e/test_completion_snapshot_int.pynow followCompletionState; the input-parent helper test expects IDs in a set instead of complete records in a list. The pre-fix failing run above records the regression evidence.Parent-index validation (Python 3.13.16, Hatch 1.18.1):
hatch run dev-otel:test -q --tb=short: 1,018 passed, including the local e2e suite.hatch run dev-otel:typecheck: 54 source files clean.hatch fmt --checkin the OTel package andgit diff --check: passed.test_natural_child_end_preserves_other_pending_childrennow asserts the remaining sibling payload and index immediately after the first natural End, then ends the second child and asserts both dictionaries are empty. It checks both child spans and their parent before parent/invocation finalization, and retains the final assertions against duplicate delivery. Existing snapshot and public-handler tests continue to cover completions without natural End hooks. This test-only change does not require rebuilding wheels or changing conformance handlers.Assumptions and limitations
Implementation decisions
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.